-
-
Notifications
You must be signed in to change notification settings - Fork 816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[deploy_website] enhance(stitch) canonical merged type/field definitions #2417
Conversation
🦋 Changeset detectedLatest commit: 1d0138e The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Also, still contemplating how to handle selecting non-empty descriptions (which is generally desirable) in a logical and non-breaking manner... |
Also, what if you want to declare a subschema as the "describer" of a field, but you want a different subschema to determine the nullability? Should we have multiple fine-gained directives? |
In terms of avoiding empty descriptions, we could just change the default merge, I guess, not the worst breaking change..... |
Good question... mainly a matter of the ye-olde-arms-race of a type already marked as important and needing to be MORE important (this is why I hate CSS). I like the simplicity of an
I'd say no... that gets more granular than the practical usecase (there's always custom merged type handlers for crazy specific things). A field should be owed by one service. Actually, going down this road, the directive could be This is sounding a lot cleaner and more intuitive. Good talk. |
See 678efaa... this is feeling a lot simpler and more intuitive. Thoughts? |
Would users also expect "canonical" fields to always be delegated to the owning service? |
I think that’s just a matter of explanation. This would strictly be a mechanism for assembling the gateway schema definition, and would have absolutely no impact on the runtime behavior. I think we could underscore that distinction in docs and make users understand the difference. The definition problem is still a difficult challenge without an easy solution right now. Also, it’s not like you need to manually specify everything as canonical. It’s really just for merged types (which naturally tend to have a primary definition in one service by nature of database design), and the option for a field override, which I’d expect to be pretty rare (we could do our entire gateway schema with just noting the base type definitions I suspect; I can’t picture a case in our stack where a field duplicated across services is more specific than one of our base types). |
Anywho... is this feeling too big or imposing for you? Not trying to push an agenda! |
Just trying to feel out implications. Let's say we later wanted to add another directive then that establishes a field as only gettable from the given subschema. Kind of a Would we be leaving enough space for that if canonical is added? Should that be a different directive? An argument to canonical? |
Okay, @yaacovCR – I'm about done with my envisioned feature set here. Open to hammering out the nitty-gritty... To your question about SDL, I think that can be broken down fairly simply by considering what each thing is fundamentally touching...
I'm with you on caution about lots of directives though... This |
We only have one directive Unless you need complex keys... The question of to use a transform versus a directive is about to get actually kind of complicated as there are a lot of transforms like filtering that might make sense as directives.... |
First pass at documentation added. I've got a few other places to work in detailed mentions, but for starters – how do you feel about how this is shaping up as a feature? |
It's growing on me! It looks like you have canonical fields overriding canonical types... Is that obviously desired behavior? In general, if two different subschemas declare a field to be canonical, I think you error instead of warning and picking one... Is that desirable? Just poking at this in my head, I think the choices you have made are pretty reasonable, just trying to figure out if there are any other options that make sense and maybe if it should be configurable.... |
I think the option for a field-level override is desirable given that
services may have unique field implementations, and therefore a lesser
service may have more to say about how one specific field works. I think
it’d be a pretty uncommon scenario, but I can see value in supporting it.
You’ll see that I didn’t do that for enums, which don’t really have any
backing implementation and therefore, only the type itself can be canonical.
Regarding the choice to error for multiple definitions, just warning and
using the final canonical definition would also make sense, and may be more
user-friendly. I could go either way on that.
Other ambitions I do have to eventually make in this area of code FWIW
while noodling on this:
- I’d like to get some warning or error system in place for divergent input
types and their enums. Having inputs diverge across services almost
certainly constitutes an API error somewhere. Providing a warning seems
like a very basic validation for the library to flag.
- Then when merging fields, I’d eventually like to make merging reconcile
nullability. If a field is nullable in one service and not in another,
seems like the gateway should be smart enough to go with the lowest-common
denominator rather than letting the developer pick a field version that
doesn’t match an underlying implementation.
So, food for thought while contemplating how schema building and validation
works!
…On Thu, Dec 31, 2020 at 12:52 PM Yaacov Rydzinski ***@***.***> wrote:
It's growing on me!
It looks like you have canonical fields overriding canonical types...
Is that obviously desired behavior? In general, if two different
subschemas declare a field to be canonical, I think you error instead of
warning and picking one... Is that desirable?
Just poking at this in my head, I think the choices you have made are
pretty reasonable, just trying to figure out if there are any other options
that make sense and maybe if it should be configurable....
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2417 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROAYQ5Q75DZMBZ4PYKTSXS25RANCNFSM4VNRKKMA>
.
|
Okay @yaacovCR – calling this ready for final review. I left the validation errors in place for now because they can always be downgraded to warnings safely (while the reverse isn't true). Also did some additional housekeeping around the doc site. Overall, I'm pretty happy with the feel of the feature (and would probably try to refactor my stack to use it!) |
No I'm thinking somewhat about the name only. A general thought: we have some directives that are nouns ( Anywa, ideas: Thoughts? |
To the suggestions –
So to expand on my thinking for <link rel="canonical" href="https://www.vox.com/culture/22218583/trump-movie-hollywood-capitol-insurrection-biden-hawley" /> It's sort of established nomenclature that the canonical definition is the thing observed above others, although no one version of the thing is technically wrong. That's pretty much a spot-on match to what this feature achieves, and that function is fairly intuitive to infer from the name alone. |
Poking at this a bit more, I’ve got a few more adjustments coming. Wrote up a doc on our own intended use cases the other day and that helped a lot to solidify what this feature needs to do. Will add some excerpts of that doc for context. To summarize further needs:
|
@@ -18,7 +18,7 @@ export function mergeEnum( | |||
: 'EnumTypeExtension', | |||
loc: e1.loc, | |||
directives: mergeDirectives(e1.directives, e2.directives, config), | |||
values: mergeEnumValues(e1.values, e2.values, config), | |||
values: mergeEnumValues(e2.values, e1.values, config), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears that the enum type and value mergers are implemented with reversed argument order from each other. While the enum type merger assigns A -> B
, the enum value merger assigns B -> A
. That means you'll end up with type settings from one candidate and value settings from the other... :sigh:
This seems like the least invasive correction to make these align.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me for now, might make sense to open up separate issue on that for consistency, defer to @ardatan
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, @yaacovCR – I've expanded this to be a lot more robust. It felt pretty squishy that this feature existed simply to service element descriptions. I've retooled some things so that it now prioritizes element definitions... those will include:
Controlling full definitions makes this a considerably more useful feature for a wide range of needs. Thinking more on the name, |
AAAAAAAARG. Rebasing totally blew out this diff. Apparently I still don't know how to rebase properly. Might just open a new branch that fixes the diff. Anyway... lots of unrelated junk in here now. Should only be about 15 files. |
git reflog to roll back rebase? |
b5dee5d
to
3a303e9
Compare
Once again in pretty good shape here and ready for consideration! |
@yaacovCR – I've thought of another major usecase that would be really nice for this feature to fulfill. I'd propose renaming the new setting/directive to Users service: type User {
id: ID!
username: String!
displayName: String
}
type Query {
users(ids: [ID!]): [User]! @merge(keyField: "id") @primary
} Reviews service: type Review {
id: ID!
body: String
author: User
}
type User {
id: ID!
reviews: [Review]!
}
type Query {
reviews(ids: [ID!]): [Review]! @merge(keyField: "id") @primary
users(ids: [ID!]): [User]! @merge(keyField: "id")
} So basically in the above, each service is now allowed to have its own Thoughts? Would this pose any dire implications to the current implementation? If it seems reasonable, where in code is this selection of root fields handled? I get quickly lost within the delegation package. |
root field names that conflict are a special case of object fields that conflict... whereas for object field names that conflict, the query planner can use whichever schema appears more optimal, for root fields, the query planner usually has to select a given schema (unless the root fields are nested), so this directive makes sense. on the other hand, the root fields could be nested -- would you want to let the query planner use whatever subschema seems most appropriate, or would you still insist on the primary? I would also wonder about using a separate directive. I think users are now going to wonder why they can't use the primary directive to select a subservice for a given field (i.e. a reverse form of provides, in which by default we can get fields from all subschemas, but allow users to choose a particular one) or maybe we should? Just thinking out loud about the connection between canonical field definitions and primary subschemas for accessing those fields... |
Hmm, that’s interesting. I can see the case for keeping schema and implementation divorced for the reasons you cite. The case for a secondary directive makes some sense. Something like an “entrypoint” or “root” setting. I’ve never used nested root types before, so I’m not super familiar with how the optimal entry process would work. All told, seems like it should favor the optimal whenever possible. |
The query planner favors a subschema that is already in the plan, to cut
down on requests, not sure if that is the case. I think the non nested root
field may be a special case that deserves it's own directive, but this is
getting tricky to explain
…On Fri, Jan 15, 2021, 12:40 AM Greg MacWilliam ***@***.***> wrote:
Hmm, that’s interesting. I can see the case for keeping schema and
implementation divorced for the reasons you cite. The case for a secondary
directive makes some sense. Something like an “entrypoint” or “root”
setting. I’ve never used nested root types before, so I’m not super
familiar with how the optimal entry process would work. All told, seems
like it should favor the optimal whenever possible.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2417 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AA7LAYHN2IYGPWQHL2R6SMLSZ5XHVANCNFSM4VNRKKMA>
.
|
I get what you’re saying. Let’s scratch this as an addendum to the canonical feature and leave this pr as is. |
Oh, I see. The query planner dynamically selects a root field to go to, doesn’t it? So in the above schema example, you can safely have both schemas define a “users” query, and it will automatically delegate to the better option based on other fields selected...? Do I have that right? If so, mind is blown YET AGAIN. I’ve been thinking of root fields as statically mapped entry points, where it will always visit the final field definition as the origin query. |
If I do have that right, then this whole topic is moot. No need to mark a preferred entrypoint if the planner is smart enough to pick the best one for the query. |
The non nested root fields are statically defined, but the nested should be treated the same as any other field (which is why btw the resolver for root fields falls back to default merged resolver if detects has an external parent) |
With non-nested being statically defined, does that follow the
final-definition-gets-the-mapping pattern? At which time, this “entrypoint”
concept would still have some value.
…On Fri, Jan 15, 2021 at 12:15 AM Yaacov Rydzinski ***@***.***> wrote:
The non nested root fields are statically defined, but the nested should
be treated the same as any other field (which is why btw the resolver for
root fields falls back to default merged resolver if detects has an
external parent)
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2417 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROGI7DMOGMTEWYWTJYDSZ7FP7ANCNFSM4VNRKKMA>
.
|
Correct, I'm just wondering if this special case deserves its own directive. Even though it is static, still seems to me that it should be separate from the other canonical issues but interested in your thoughts |
Well, given where we’ve arrived at here, I think I’m back at my original
thinking on the subject: which is that I can’t picture a scenario where I’d
want to promote a primary field definition to the gateway schema without it
also being the source of the root query. While I agree that it fuzzes the
line of definition versus implementation a bit in the case of root fields,
having a root schema definition not bring its resolver mapping along with
it feels potentially more unintuitive to me. I think it could be explained
that root fields are a special case. As a separate directive, I suspect the
two setting would virtually always used together.
…On Fri, Jan 15, 2021 at 12:35 AM Yaacov Rydzinski ***@***.***> wrote:
Correct, I'm just wondering if this special case deserves its own
directive. Even though it is static, still seems to me that it should be
separate from the other canonical issues but interested in your thoughts
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2417 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROHXQ65SVGLEMPS6P43SZ7HZDANCNFSM4VNRKKMA>
.
|
Makes sense to me! Docs should highlight. |
Hu, looks like the way this sorts everything already lined it all up so that the canonical root definition is the one that gets used. I just tinkered around with permutations in this test, and they all work as expected! 1a1b332. Does it make sense that this works? I assume candidate order must be preserved within the delegation mappings. The one exception is that resolvers built into the stitched schema will override the primary delegations, which seems... fine. So I guess, rename all this to |
Yes, last root field wins, so changing the order would probably be enough. Does |
I was thinking primary because of it specifying the default root field, but
you’re right, that too gets into loaded assumptions. Canonical remains
fairly unassuming as “canon of the gateway schema”, which still makes sense
for default root field targets. We’ll stick with that!
Will get the docs updated to match.
…On Sat, Jan 16, 2021 at 3:22 PM Yaacov Rydzinski ***@***.***> wrote:
Yes, last root field wins, so changing the order would probably be enough.
Does @primary imply that there could be some "other" scenario where a
secondary is used? I think @canonical still makes sense.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#2417 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFRROHSODEJBWKXEZAGT53S2HYPBANCNFSM4VNRKKMA>
.
|
Docs are updated! Once again, I think we're in good shape here. |
Follows up the discussion of better support for merging type and field descriptions in gmac/schema-stitching-handbook#12. Interestingly, Federation actually has an approach to this problem given that their use of the "extends" keyword establishes a base for each type to prioritize.
This proposal more or less takes the inverse approach of the
extends
keyword... rather thanextends
identifying descriptions that we don't prioritize, let's simply flag the types we do want to reference as being "canonical".Merge Candidates changes
canonical
setting toMergedTypeConfig
and the informal "MergedFieldConfig" type.None of these changes are breaking from the current behavior. 🙌
Stitching Directives changes
Adds a
@canonical
directive that can be applied to all merged schema types and fields.Under the hood, these directives just write
canonical
attributes into merged type config for types and their fields. For simplicity, Enum values cannot override the canonical enum definition, which seems... fine (wanting to document the official values in two places without a backing implementation concern is an extreme edge case).@yaacovCR – thoughts on this? If you're on board with the feature, then I'll proceed with adding the corresponding docs. If not, we can table it at this stage. All told, I think this is a fairly elegant library-level solution to the descriptions problem.
TODO: